package io.shuttle.mbe.core

import android.net.Uri
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import androidx.core.net.toUri
import androidx.webkit.WebMessageCompat


fun String?.toJSObject(): JSObject? {
    if (this.isNullOrBlank()) return null
    return JSObject(this)
}

fun Any?.toUri(): Uri? {
    val data = if (this is String?) {
        this
    } else {
        this.toString()
    }
    if (data.isNullOrBlank()) return null
    return data.toUri()
}


fun Map<String, Any?>?.toJSObject(): JSObject? {
    if (this.isNullOrEmpty()) return null
    return JSObject().also {
        for (entry in entries) {
            it.putOpt(entry.key, entry.value)
        }
    }
}

typealias WabMessageHandlerFunction = (
    message: WebMessageCompat,
    postMessageBody: PostMessageBody?,
    sourceOrigin: Uri?,
    isMainFrame: Boolean,
    reply: WabMessageReply?
) -> Unit

sealed class Ability {
    data class Value(var data: Any?) : Ability()
    data class Function(var handle: WabMessageHandlerFunction) : Ability()
}

interface WabMessageReply {
    fun replyJs(
        replyBody: ReplyBody,
        wabSupportType: WabSupportType,
    )
}

enum class WabSupportType {
    Legacy,
    Listener,
    Channel
}

enum class PostMessageType {
    Function,
    Value
}

val kExcludeParameterKeys = arrayOf<String>("type", "name", "replyId", "replyOnce")


data class PostMessageBody(
    val type: PostMessageType = PostMessageType.Function,
    val name: String? = null,
    val replyId: String? = null,
    val replyOnce: Boolean = true,
    var args: MutableMap<String, Any>? = null
) {
    companion object Companion {
        fun fromJson(json: String?): PostMessageBody? {
            if (json.isNullOrBlank()) return null
            Logger.d("[PostNativeRequest.fromJson] $json")
            return try {
                val obj = JSObject(json)
                val data = PostMessageBody(
                    type = obj.getString("type", null)?.let {
                        if (it == "value") PostMessageType.Value
                        else if (it == "function") PostMessageType.Function
                        else throw IllegalArgumentException("Invalid type: $it")
                    }
                        ?: PostMessageType.Function,
                    name = obj.getString("name"),
                    replyId = obj.getString("replyId", null),
                    replyOnce = obj.getBoolean("replyOnce", false)!!,
                    args = mutableMapOf()
                )
                obj.keys()
                    .forEach {
                        if (!kExcludeParameterKeys.contains(it)) {
                            data.args!![it] = obj.get(it)
                        }
                    }
                data
            } catch (e: Exception) {
                Logger.e("[PostNativeRequest.fromJson] ${e.message}")
                null
            }
        }
    }

    fun toJson(): JSObject {
        return JSObject().also {
            it.putOpt("type", type.name)
            it.putOpt("name", name)
            it.putOpt("replyId", replyId)
            it.putOpt("replyOnce", replyOnce)
            if (args != null) {
                for (entry in args!!.entries) {
                    it.putOpt(entry.key, entry.value)
                }
            }
        }
    }

    override fun toString(): String {
        return toJson().toString()
    }
}

data class ReplyBody(
    val error: Exception? = null,
    val data: Any? = null,
    var replyOnce: Boolean = true,
    var replyId: String? = null,
    var args: MutableMap<String, Any>? = null
) {
    companion object {
        fun fromJson(json: String?): ReplyBody? {
            if (json.isNullOrBlank()) return null
            return try {
                val obj = JSObject(json)
                val data = ReplyBody(
                    error = Exception(obj.getString("error")),
                    data = obj.get("data"),
                    replyOnce = obj.getBoolean("replyOnce"),
                    replyId = obj.getString("replyId"),
                    args = mutableMapOf()
                )
                obj.keys().forEach {
                    if (!kExcludeParameterKeys.contains(it)) {
                        data.args!![it] = obj.get(it)
                    }
                }
                data
            } catch (e: Exception) {
                Logger.e("[ReplyBody.fromJson] ${e.message}")
                null
            }
        }
    }

    fun toJson(): JSObject {
        return JSObject().also {
            it.putOpt("error", error?.message)
            it.putOpt("data", data)
            it.putOpt("replyOnce", replyOnce)
            it.putOpt("replyId", replyId)
            if (args != null) {
                for (entry in args!!.entries) {
                    it.putOpt(entry.key, entry.value)
                }
            }
        }
    }

    override fun toString(): String {
        return toJson().toString()
    }
}

class JSObject : JSONObject {
    constructor() : super()
    constructor(json: String) : super(json)
    constructor(copyFrom: JSObject, names: Array<String>) : super(copyFrom, names)

    companion object {
        @Throws(JSONException::class)
        fun fromJSONObject(obj: JSONObject): JSObject {
            val keysIter = obj.keys()
            val keys = mutableListOf<String>()
            while (keysIter.hasNext()) {
                keys.add(keysIter.next())
            }
            return JSObject(obj as JSObject, keys.toTypedArray())
        }
    }

    fun getString(key: String, defaultValue: String?): String? {
        return try {
            super.getString(key) ?: defaultValue
        } catch (e: JSONException) {
            Logger.e("[JSObject.getString] ${e.message}, default value: $defaultValue")
            defaultValue
        }
    }

    fun getInt(key: String, defaultValue: Int?): Int? {
        return try {
            super.getInt(key)
        } catch (e: JSONException) {
            Logger.e("[JSObject.getInt] ${e.message}, default value: $defaultValue")
            defaultValue
        }
    }

    fun getBoolean(key: String, defaultValue: Boolean?): Boolean? {
        return try {
            super.getBoolean(key)
        } catch (e: JSONException) {
            Logger.e("[JSObject.getBoolean] ${e.message}, default value: $defaultValue")
            defaultValue
        }
    }

    fun getJSObject(name: String): JSObject? {
        return getJSObject(name, null)
    }

    fun getJSObject(key: String, defaultValue: JSObject?): JSObject? {
        return try {
            val obj = get(key)
            if (obj is JSONObject) {
                val keysIter = (obj as JSObject).keys()
                val keys = mutableListOf<String>()
                while (keysIter.hasNext()) {
                    keys.add(keysIter.next())
                }
                return JSObject(obj, keys.toTypedArray())
            }
            defaultValue
        } catch (e: JSONException) {
            Logger.e("[JSObject.getJSObject] ${e.message}, default value: $defaultValue")
            defaultValue
        }
    }


    fun put(name: String, value: Boolean?): JSObject {
        return try {
            super.put(name, value) as JSObject
        } catch (e: JSONException) {
            this
        }
    }

    fun put(name: String, value: Int?): JSObject {
        return try {
            super.put(name, value) as JSObject
        } catch (e: JSONException) {
            this
        }
    }

    fun put(name: String, value: Long?): JSObject {
        return try {
            super.put(name, value) as JSObject
        } catch (e: JSONException) {
            this
        }
    }

    fun put(name: String, value: Double?): JSObject {
        return try {
            super.put(name, value) as JSObject
        } catch (e: JSONException) {
            this
        }
    }

    fun put(name: String, value: String?): JSObject {
        return try {
            super.put(name, value) as JSObject
        } catch (e: JSONException) {
            this
        }
    }

    override fun put(name: String, value: Any?): JSObject {
        return try {
            super.put(name, value) as JSObject
        } catch (e: JSONException) {
            this
        }
    }
}

class JSArray : JSONArray {
    constructor() : super()
    constructor(json: String?) : super(json)
    constructor(copyFrom: MutableCollection<*>?) : super(copyFrom)
    constructor(array: Any?) : super(array)

    @Throws(JSONException::class)
    fun <E> toList(): MutableList<E?> {
        val items: MutableList<E?> = ArrayList<E?>()
        var o: Any? = null
        for (i in 0..<this.length()) {
            o = this.get(i)
            try {
                items.add(this.get(i) as E?)
            } catch (ex: Exception) {
                throw JSONException("Not all items are instances of the given type")
            }
        }
        return items
    }

    companion object {
        /**
         * Create a new JSArray without throwing a error
         */
        fun from(array: Any?): JSArray? {
            return try {
                JSArray(array)
            } catch (ex: JSONException) {
                Logger.e("[JSArray.from] ${ex.message}")
                null
            }
        }
    }
}
